﻿// <copyright file="VirtualMatrix.cs" company="Signal Processing Lab, Wuhan University">
// Copyright (c) 2009-2010 All Right Reserved
// </copyright>
// <author>Xinping Deng</author>
// <email>dentoby@gmail.com</email>
// <date>2010-4</date>
// <summary>This is part of Image.NET project.</summary>

namespace SPLWHU.Core.Raster
{
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using SPLWHU.Core.Exceptions;

    /// <summary>
    /// The matrix with large size.
    /// </summary>
    /// <typeparam name="T">The data type.</typeparam>
    public class VirtualMatrix<T> : Matrix<T>
    {
        /// <summary>
        /// The data buffer in memory.
        /// </summary>
        private Matrix<T> buffer;

        /// <summary>
        /// The first row index.
        /// </summary>
        private int rowFirst;

        /// <summary>
        /// The last row index.
        /// </summary>
        private int rowLast;

        /// <summary>
        /// The height of block to be read.
        /// </summary>
        private int blockHeight;

        /// <summary>
        /// The number of rows overlapped.
        /// </summary>
        private int overlap = 10;

        /// <summary>
        /// Determines whether the buffer is synthronous to the provider.
        /// </summary>
        private bool isSynthronous = true;

        /// <summary>
        /// Determines whether the buffer is changed or not.
        /// </summary>
        private bool buffChanged = false;

        /// <summary>
        /// The data provider.
        /// </summary>
        private IDataProvider<T> provider;

        /// <summary>
        /// Initializes a new instance of the <see cref="VirtualMatrix&lt;T&gt;"/> class.
        /// </summary>
        /// <param name="rows">The number of rows.</param>
        /// <param name="columns">The number of columns.</param>
        public VirtualMatrix(int rows, int columns)
            : base(rows, columns)
        {
            this.rowFirst = 0;
            this.rowLast = -1;
            this.buffer = null;
            this.blockHeight = Matrix<T>.MaxSize / columns;

            this.provider = new GdalProvider<T>(columns, rows, (int)this.GetFlag());
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="VirtualMatrix&lt;T&gt;"/> class.
        /// </summary>
        /// <param name="provider">The data provider.</param>
        public VirtualMatrix(IDataProvider<T> provider)
            : base(provider.ImageHeight, provider.ImageWidth)
        {
            this.rowFirst = 0;
            this.rowLast = -1;
            this.buffer = null;
            this.blockHeight = Matrix<T>.MaxSize / this.Columns;
            this.provider = provider;
        }

        /// <summary>
        /// Gets or sets the value with the specified position.
        /// </summary>
        /// <param name="x">The x column index.</param>
        /// <param name="y">The y row index.</param>
        /// <returns>The value.</returns>
        public override T this[int x, int y]
        {
            get
            {
                if (y < this.rowFirst || y >= this.rowLast)
                {
                    this.RefreshBuffer(y);
                }
                else
                {
                    if (!this.isSynthronous)
                    {
                        this.ReadBuffer();
                    }
                }

                return this.buffer[x, y + this.rowFirst];
            }

            set
            {
                if (y < this.rowFirst || y >= this.rowLast)
                {
                    this.RefreshBuffer(y);
                }
                else
                {
                    if (!this.isSynthronous)
                    {
                        this.ReadBuffer();
                    }
                }

                this.buffer[x, y + this.rowFirst] = value;
                this.buffChanged = true;
            }
        }

        /// <summary>
        /// Gets or sets the row at the specified postion.
        /// </summary>
        /// <param name="y">The row index.</param>
        /// <returns>The row data.</returns>
        public override T[] this[int y]
        {
            get
            {
                if (y < this.rowFirst || y >= this.rowLast)
                {
                    this.RefreshBuffer(y);
                }
                else
                {
                    if (!this.isSynthronous)
                    {
                        this.ReadBuffer();
                    }
                }

                return this.buffer[y + this.rowFirst];
            }

            set
            {
                if (y < this.rowFirst || y >= this.rowLast)
                {
                    this.RefreshBuffer(y);
                }
                else
                {
                    if (!this.isSynthronous)
                    {
                        this.ReadBuffer();
                    }
                }

                this.buffer[y + this.rowFirst] = value;
                this.buffChanged = true;
            }
        }

        /// <summary>
        /// Gets the specified column.
        /// </summary>
        /// <param name="x">The column index.</param>
        /// <returns>The column data.</returns>
        public override T[] GetColumn(int x)
        {
            if (x < 0 || x > this.Rows)
            {
                throw new MatrixException("Requires row {0} from Matrix {1}", x, this.ToString());
            }

            // updates the provider first.
            if (this.buffChanged)
            {
                this.WriteBuffer();
            }

            // reads data.
            return this.provider.GetColumn(x);
        }

        /// <summary>
        /// Sets the data to the specified column.
        /// </summary>
        /// <param name="x">The column index.</param>
        /// <param name="data">The column data.</param>
        public override void SetColumn(int x, T[] data)
        {
            if (x < 0 || x > this.Rows)
            {
                throw new MatrixException("Requires row {0} from Matrix {1}", x, this.ToString());
            }

            // updates the provider first.
            if (this.buffChanged)
            {
                this.WriteBuffer();
            }

            // writes data.
            this.provider.SetColumn(x, data);

            // the current buffer is out date.
            this.isSynthronous = false;
        }

        /// <summary>
        /// Gets the data of this instance in a bounding box (The referenc coordinate is the left-top point).
        /// </summary>
        /// <param name="left">The left side of the bounding box.</param>
        /// <param name="top">The top side of the bounding box.</param>
        /// <param name="right">The right side of the bounding box.</param>
        /// <param name="bottom">The bottom side of the bounding box.</param>
        /// <returns>The data in the bounding box.</returns>
        public override Matrix<T> GetChild(int left, int top, int right, int bottom)
        {
            // Makes sure the left is less than right.
            if (left > right)
            {
                int swap = right;
                right = left;
                left = swap;
            }

            // Makes sure the top is less than the bottom.
            if (top > bottom)
            {
                int swap = bottom;
                bottom = top;
                top = swap;
            }

            // Makes sure the boundings is not out of range.
            if (left < 0 || left >= this.Columns || right >= this.Columns || top < 0 || top >= this.Rows || bottom >= this.Rows)
            {
                throw new MatrixException("Requires [{0}, {1}] ~ [{2}, {3}] from {4}.", left, top, right, bottom, this.ToString());
            }

            // updates the provider first.
            if (this.buffChanged)
            {
                this.WriteBuffer();
            }

            // reads data.
            return this.provider.GetData(left, top, bottom - top, right - left);
        }

        /// <summary>
        /// Sets the data in a matrix to the position of this instance.
        /// </summary>
        /// <param name="left">The left side.</param>
        /// <param name="top">The top side.</param>
        /// <param name="source">The source data.</param>
        public override void SetChild(int left, int top, Matrix<T> source)
        {
            int w = source.Columns;
            int h = source.Rows;

            // Checks the range.
            if (left < 0 || (w + left) > this.Columns || top < 0 || (h + top) > this.Rows)
            {
                throw new MatrixException("Requires [{0}, {1}] ~ [{2}, {3}] from {4}.", left, w + left, top, h + top, this.ToString());
            }

            // updates the provider first.
            if (this.buffChanged)
            {
                this.WriteBuffer();
            }

            // writes data.
            this.provider.SetData(left, top, source);

            // the current buffer is out of date.
            this.isSynthronous = false;
        }

        /// <summary>
        /// Gets the data of this instance in a bounding box (The referenc coordinate is the left-top point).
        /// </summary>
        /// <param name="left">The left side of the bounding box.</param>
        /// <param name="top">The top side of the bounding box.</param>
        /// <param name="right">The right side of the bounding box.</param>
        /// <param name="bottom">The bottom side of the bounding box.</param>
        /// <param name="dstWidth">Width of the destination region.</param>
        /// <param name="dstHeight">Height of the destionation region.</param>
        /// <returns>The data in the bounding box.</returns>
        public override Matrix<T> GetChild(int left, int top, int right, int bottom, int dstWidth, int dstHeight)
        {
            // Makes sure the left is less than right.
            if (left > right)
            {
                int swap = right;
                right = left;
                left = swap;
            }

            // Makes sure the top is less than the bottom.
            if (top > bottom)
            {
                int swap = bottom;
                bottom = top;
                top = swap;
            }

            // Makes sure the boundings is not out of range.
            if (left < 0 || left >= this.Columns || right >= this.Columns || top < 0 || top >= this.Rows || bottom >= this.Rows)
            {
                throw new MatrixException("Requires [{0}, {1}] ~ [{2}, {3}] from {4}: index is out of range.", left, top, right, bottom, this.ToString());
            }

            if (dstWidth <= 0 || dstHeight <= 0)
            {
                throw new MatrixException("Width [{0}] or height [{1}] is invalid.", dstWidth, dstHeight);
            }

            // updates the provider first.
            if (this.buffChanged)
            {
                this.WriteBuffer();
            }

            // reads data.
            return this.provider.GetData(left, top, bottom - top, right - left, dstHeight, dstWidth);
        }

        /// <summary>
        /// Sets the data in a matrix to the position of this instance.
        /// </summary>
        /// <param name="left">The left side.</param>
        /// <param name="top">The top side.</param>
        /// <param name="dstWidth">Width of in this matrix.</param>
        /// <param name="dstHeight">Height of in this matrix.</param>
        /// <param name="source">The source data.</param>
        public override void SetChild(int left, int top, int dstWidth, int dstHeight, Matrix<T> source)
        {
            // Checks the range.
            if (left < 0 || (dstWidth + left) > this.Columns || top < 0 || (dstHeight + top) > this.Rows)
            {
                throw new MatrixException("Requires [{0}, {1}] ~ [{2}, {3}] from {4}.", left, dstWidth + left, top, dstHeight + top, this.ToString());
            }

            // updates the provider first.
            if (this.buffChanged)
            {
                this.WriteBuffer();
            }

            // writes data.
            this.provider.SetData(left, top, dstHeight, dstWidth, source);

            // the current buffer is out of date.
            this.isSynthronous = false;
        }

        /// <summary>
        /// Fills the specified data.
        /// </summary>
        /// <param name="data">The data to be filled.</param>
        public override void Fill(T[][] data)
        {
            if (data.Length != this.Rows || data[0].Length != this.Columns)
            {
                throw new MatrixException("The size of {0} and array [{1} {2}] do not meet.", this.ToString(), data.Length, data[0].Length);
            }

            // writes data
            this.provider.SetData(0, 0, data);

            // the current buffer is out of date.
            this.isSynthronous = false;
        }

        /// <summary>
        /// Allocates memory for Matrix data.
        /// </summary>
        public override void Allocate()
        {
            // do nothing.
        }

        /// <summary>
        /// Updates the buffer to the provider.
        /// </summary>
        private void WriteBuffer()
        {
            this.provider.SetData(0, this.rowFirst, this.buffer);
            this.buffChanged = false;
        }

        /// <summary>
        /// Reads the buffer data from the provider.
        /// </summary>
        private void ReadBuffer()
        {
            this.buffer = this.provider.GetData(0, this.rowFirst, this.rowLast - this.rowFirst, this.Columns);
            this.buffChanged = false;
        }

        /// <summary>
        /// Refreshes the buffer.
        /// </summary>
        /// <param name="y">The current row index.</param>
        private void RefreshBuffer(int y)
        {
            // Updates the modified data.
            if (this.buffChanged)
            {
                this.WriteBuffer();
            }

            // Updates the row offset.
            this.rowFirst = y - this.overlap;
            if (this.rowFirst < 0)
            {
                this.rowFirst = 0;
            }

            this.rowLast = this.rowFirst + this.blockHeight;
            if (this.rowLast > this.Rows)
            {
                this.rowLast = this.Rows;
            }

            // Reads the data.
            this.ReadBuffer();
        }

        /// <summary>
        /// Gets the type flag <see cref="TypeFlat"/>.
        /// </summary>
        /// <returns>The type flag.</returns>
        private TypeFlag GetFlag()
        {
            if (typeof(T) == typeof(byte))
            {
                return TypeFlag.TF_Byte;
            }
            else if (typeof(T) == typeof(ushort))
            {
                return TypeFlag.TF_UShort;
            }
            else if (typeof(T) == typeof(int))
            {
                return TypeFlag.TF_Int;
            }
            else if (typeof(T) == typeof(uint))
            {
                return TypeFlag.TF_UInt;
            }
            else if (typeof(T) == typeof(float))
            {
                return TypeFlag.TF_Float;
            }
            else if (typeof(T) == typeof(double))
            {
                return TypeFlag.TF_Double;
            }
            else
            {
                return TypeFlag.TF_Unkown;
            }
        }
    }
}
